/**
 * 
 */
package org.gpp.proj1.map;

import java.util.Collection;
import java.util.List;
import java.util.Scanner;

import org.gpp.proj1.logic.core.board.RobotPosition;

import com.jme.bounding.BoundingBox;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.AlphaState;
import com.jme.scene.state.LightState;
import com.jme.system.DisplaySystem;

/**
 * A node that carries a map and some util information about the map,
 * like heights and modifiers.
 * @author Gustavo de Farias
 */
public class MapNode extends Node {
	
	// the render side of a map block
	public static final int TILE_SIZE = 24;
	// the render height of a map block
	public static final int TILE_HEIGHT = TILE_SIZE / 6;
	// the gap between two selection tiles
	private static final float INTERTILE_GAP = 1.0f;
	// the offset of selection tile above block surface
	private static final float HEIGHT_OFFSET = 0.05f;
	// blocks with this modifier will not be selectable
	private static final byte INACCESSIBLE_BLOCK_MODIFIER = -1;

	/**
	 * 
	 */
	private static final long serialVersionUID = -5758727453699890058L;
	
	// original information used to build the map
	private byte[][] heights;
	private byte[][] modifiers;
	private int height;
	
	// node for selection and cursor tiles
	private Node selectionTiles;
	private Quad cursorTile;
	// store the state of selection tiles (showing or hidden)
	private boolean movementSelectorOpened;
	private boolean attackSelectorOpened;
	
	/**
	 * Constructs a Map Node.
	 * @param name Name for the node.
	 * @param heights
	 * @param modifiers
	 */
	public MapNode(String name, byte[][] heights, byte[][] modifiers, int height) {
		super(name);
		
		this.heights = heights;
		this.modifiers = modifiers;
		
		this.movementSelectorOpened = false;
		this.attackSelectorOpened = false;
		
		this.height = height;
	}
	
	/**
	 * @param name
	 */
	public MapNode(String name) {
		super(name);
	}
	
	/**
	 * @return
	 */
	public byte[][] getHeights() {
		return heights;
	}
	
	/**
	 * @return
	 */
	public byte[][] getModifiers() {
		return modifiers;
	}
	
	public void activateCursorTile(int l, int c, ColorRGBA color) {
		
		// remove previous cursor tile
		this.deactivateCursorTile();
		
		this.cursorTile = new Quad( "Cursor " + l + ", " + c, MapNode.TILE_SIZE, MapNode.TILE_SIZE);
		
		cursorTile.setLocalTranslation(getTileTranslation(l, c));
		cursorTile.setModelBound(new BoundingBox());
		cursorTile.updateModelBound();
		cursorTile.setSolidColor(color);
				
		// make tile translucent
		AlphaState as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
		as.setBlendEnabled(true);
	    as.setEnabled(true);
		
	    cursorTile.setRenderState(as);
		cursorTile.updateRenderState();
		cursorTile.updateGeometricState( 0, true );
		
		this.attachChild( cursorTile );
		this.updateGeometricState(0, true);
	}
	
	/**
	 * Disable cursor tile.
	 */
	public void deactivateCursorTile() {
		if ( this.cursorTile != null ) {
			this.detachChild( this.cursorTile );
			this.cursorTile = null;
		}
	}

	/**
	 * Disable cursor tile in a specific location.
	 * @param l
	 * @param c
	 */
	public void deactivateCursorTile(int l, int c) {
		
		this.detachChildNamed( "Cursor " + l + ", " + c );
		this.updateGeometricState(0, true);
	}
	
	/**
	 * Creates a selection region around a block.
	 * @param l block line
	 * @param c block column
	 * @param range The range of selection region
	 * @param color The color or region tiles
	 * @param name 
	 */
	public void activateSelectionTiles(int l, int c, int range, ColorRGBA color, List<RobotPosition> exceptions) {		
		// remove previous region
		this.deactivateSelectionRegion();
		
		// selection region node
		this.selectionTiles = new Node(getSelectionTilesName(l, c));
		selectionTiles.setLightCombineMode(LightState.OFF);
		
		// initially, no blocks were visited
		boolean[][] visited = new boolean[this.heights.length][this.heights[0].length];
		for( int k = 0; k < this.heights.length; k++ ) {
			for( int i = 0; i < this.heights[0].length; i++ ) {
				visited[k][i] = false;
			}
		}
		// eliminate exceptions from receiving a tile
		for( RobotPosition exception : exceptions ) {
			if( exception.getX() != c && exception.getY() != l ) {
				visited[exception.getY()][exception.getX()] = true;
			}
		}
		
		// start spreading of tiles
		this.activateTiles( l, c, range, color, 0, visited );
		
		// make region translucent
		AlphaState as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
		as.setDstFunction( AlphaState.DB_SRC_ALPHA );
		as.setBlendEnabled(true);
	    as.setEnabled(true);
	    
		selectionTiles.setRenderState(as);
		selectionTiles.updateRenderState();
		selectionTiles.updateGeometricState(0, true);

		// add region to the map
		this.attachChild(selectionTiles);
		this.updateGeometricState(0, true);
		this.updateRenderState();
		
		this.movementSelectorOpened = true;
	}

	/**
	 * Spread selection tiles around a coordinate until
	 * reach a specific range.
	 * @param l
	 * @param c
	 * @param range the range of the selection area.
	 * @param color
	 * @param depth Since this method is recursive, the depth
	 * indicates how far the spreading is between the origin
	 * and the range.
	 * @param visited the blocks already visited
	 */
	private void activateTiles( int l, int c, int range, ColorRGBA color, int depth, boolean[][] visited) {
		// the stop condition of recursion
		if( ! validPosition(l, c) // unreachable block
			|| depth > range // reached the range
			|| visited[l][c] ) { // Another call came first
			return;
		}
		
		if( depth != 0 ) { // the origin does not reaceave a tile
			Quad tile = new Quad( getQuadName(l, c), MapNode.TILE_SIZE - MapNode.INTERTILE_GAP, MapNode.TILE_SIZE - MapNode.INTERTILE_GAP);
			// tile positioning
			tile.setLocalTranslation(getTileTranslation(l, c));
			tile.setModelBound(new BoundingBox());
			tile.updateModelBound();
			tile.setSolidColor(color);					
			
			tile.updateRenderState();
			tile.updateGeometricState( 0, true );
			// add to node
			selectionTiles.attachChild(tile);
		}
		
		// this block is done
		visited[l][c] = true;
		
		// the contamination continue in the neighborhood
		activateTiles( l - 1, c, range, color, depth + 1, visited);
		activateTiles( l + 1, c, range, color, depth + 1, visited);
		activateTiles( l, c - 1, range, color, depth + 1, visited);
		activateTiles( l, c + 1, range, color, depth + 1, visited);
		
		visited[l][c] = false;
	}
	
	public void activateAttackSelectionTiles(int l, int c, int range, ColorRGBA color, Collection<RobotPosition> positions, Collection<RobotPosition> exceptions) {		
		// remove previous region
		this.deactivateSelectionRegion();
		
		// selection region node
		this.selectionTiles = new Node(getSelectionTilesName(l, c));
		selectionTiles.setLightCombineMode(LightState.OFF);
		
		boolean[][] visited = new boolean[this.heights.length][this.heights[0].length];
		boolean[][] targets = new boolean[this.heights.length][this.heights[0].length];
		for( int k = 0; k < this.heights.length; k++ ) {
			for( int i = 0; i < this.heights[0].length; i++ ) {
				visited[k][i] = false;
				targets[k][i] = false;
			}
		}
		for( RobotPosition position : positions ) {
			targets[position.getY()][position.getX()] = true;
		}
//		for( RobotPosition position : exceptions ) {
//			targets[position.getY()][position.getX()] = false;
//		}

		this.activateAttackTiles( l, c, range, color, 0, visited, targets );

		// make region translucent
		AlphaState as = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
		as.setDstFunction( AlphaState.DB_SRC_ALPHA );
		as.setBlendEnabled(true);
	    as.setEnabled(true);
	    
		selectionTiles.setRenderState(as);
		selectionTiles.updateRenderState();
		selectionTiles.updateGeometricState(0, true);

		// add region to the map
		this.attachChild(selectionTiles);
		this.updateGeometricState(0, true);
		this.updateRenderState();
		
		this.attackSelectorOpened = true;
	}
	
	private void activateAttackTiles( int l, int c, int range, ColorRGBA color, int depth, boolean[][] visited, boolean[][] targets) {
		
		if( ! validPosition(l, c)
			|| depth > range
			|| visited[l][c] ) {
			return;
		}
		
		if( depth != 0 && targets[l][c] ) {
			Quad tile = new Quad( getQuadName(l, c), MapNode.TILE_SIZE - MapNode.INTERTILE_GAP, MapNode.TILE_SIZE - MapNode.INTERTILE_GAP);
			// tile positioning
			tile.setLocalTranslation(getTileTranslation(l, c));
			tile.setModelBound(new BoundingBox());
			tile.updateModelBound();
			tile.setSolidColor(color);					
			
			tile.updateRenderState();
			tile.updateGeometricState( 0, true );
			// add to node
			selectionTiles.attachChild(tile);
		}
		
		visited[l][c] = true;
				
		activateAttackTiles( l - 1, c, range, color, depth + 1, visited, targets);
		activateAttackTiles( l + 1, c, range, color, depth + 1, visited, targets);
		activateAttackTiles( l, c - 1, range, color, depth + 1, visited, targets);
		activateAttackTiles( l, c + 1, range, color, depth + 1, visited, targets);
		
		visited[l][c] = false;
	}

	/**
	 * Generate a name for a new Quad based on its coordinates.
	 * @param line Line
	 * @param col Column
	 * @return
	 */
	private String getQuadName(int line, int col) {
		return line + ":" + col;
	}
	
	/**
	 * Translate a tile to the correct position according to
	 * map information.
	 * @param line
	 * @param col
	 * @return
	 */
	private Vector3f getTileTranslation(int line, int col) {
		// TODO Quando a orienta��o dos eixos for corrigida
		// � preciso trocar o Y com o Z.
		return new Vector3f(col * MapNode.TILE_SIZE,
							this.height - line * MapNode.TILE_SIZE,
							this.heights[line][col] * MapNode.TILE_HEIGHT + MapNode.HEIGHT_OFFSET);
	}

	/**
	 * Determines if a given block of map is selectable.
	 * @param line
	 * @param col
	 * @return
	 */
	private boolean validPosition(int line, int col) {
		return line < heights.length && line >= 0 && // tests bounds
			   col < heights[0].length && col >=0 && // tests bounds
			   // test if block is accessible
			   this.modifiers[line][col] != MapNode.INACCESSIBLE_BLOCK_MODIFIER;
	}

	/**
	 * Deactivate a selection region
	 * @param l line 
	 * @param c column
	 */
	public void deactivateSelectionTiles(int l, int c) {
		
		// remove the region node from map
		this.detachChildNamed(getSelectionTilesName(l, c));
		this.updateGeometricState(0, true);
	}
	
	/**
	 * Disable selection region.
	 */
	public void deactivateSelectionRegion() {
		if ( this.selectionTiles != null ) {
			// remove from map node
			this.detachChild( this.selectionTiles );
			// remove tiles (may be unnecessary)
			this.selectionTiles.detachAllChildren();
			this.selectionTiles = null;
			
			this.movementSelectorOpened = false;
			this.attackSelectorOpened = false;
		}
	}
	
	/**
	 * Gets the name of a selection tile.
	 * @param l line
	 * @param c column
	 * @return the name of a selection tile.
	 */
	private String getSelectionTilesName(int l, int c) {
		return "Selection " + l + ", " + c;
	}
	
	/**
	 * Obtain map coordinates (line, column) for a selection tile
	 * based on the name of the Quad that represents the tile.
	 * @param tileName Name of the Quad mesh.
	 * @return The coordinates of the selection tile on this map.
	 * Array[0] = line, Array[1] = column.
	 */
	public int[] getTilePositionInMap( String tileName ) {
		int[] coord = new int[2];

		Scanner s = new Scanner(tileName);
		s.useDelimiter(":");
		coord[0] = s.nextInt(); // line
		coord[1] = s.nextInt(); // column
		
		return coord;
	}
	
	/**
	 * Obtain the node for the selection tiles. May be null
	 * if no selection region is showing.
	 * @return
	 */
	public Node getSelectionTiles() {
		return selectionTiles;
	}

	public boolean isMovementSelectorOpen() {

		return selectionTiles != null && this.movementSelectorOpened;
	}
	
	public boolean isAttackSelectorOpen() {

		return selectionTiles != null && this.attackSelectorOpened;
	}
}